Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Creates project for test execution #2010

Merged
merged 6 commits into from
Mar 13, 2024

Conversation

lantoli
Copy link
Member

@lantoli lantoli commented Mar 12, 2024

Description

Creates project for test execution. As an example it's used for privatelink_endpoint_service_data_federation_online_archive resource tests.

Link to any related issue(s): CLOUDP-237263

This creates a project for each resource test execution (if needed), example log:

Creating execution project: test-acc-tf-p-430893557017739723, resource: privatelinkendpointservicedatafederationonlinearchive, global project (not found): test-acc-tf-p-keep-privatelinkendpointservicedatafederationonlin
--- PASS: TestAccNetworkPrivatelinkEndpointServiceDataFederationOnlineArchive_basic (11.40s)
=== RUN   TestAccNetworkPrivatelinkEndpointServiceDataFederationOnlineArchive_basicWithRegionDnsName
--- PASS: TestAccNetworkPrivatelinkEndpointServiceDataFederationOnlineArchive_basicWithRegionDnsName (8.42s)
PASS
Deleting execution project: test-acc-tf-p-430893557017739723, resource: privatelinkendpointservicedatafederationonlinearchive

Optionally it can use an existing project for that resource if it exists, example log:

Reusing global project: test-acc-tf-p-keep-privatelinkendpointservicedatafederationonlin, resource: privatelinkendpointservicedatafederationonlinearchive

Type of change:

  • Bug fix (non-breaking change which fixes an issue). Please, add the "bug" label to the PR.
  • New feature (non-breaking change which adds functionality). Please, add the "enhancement" label to the PR.
  • Breaking change (fix or feature that would cause existing functionality to not work as expected). Please, add the "breaking change" label to the PR.
  • This change requires a documentation update
  • Documentation fix/enhancement

Required Checklist:

  • I have signed the MongoDB CLA
  • I have read the contribution guidelines
  • I have checked that this change does not generate any credentials and that they are NOT accidentally logged anywhere.
  • I have added tests that prove my fix is effective or that my feature works per HashiCorp requirements
  • I have added any necessary documentation (if appropriate)
  • I have run make fmt and formatted my code
  • If changes include deprecations or removals, I defined an isolated PR with a relevant title as it will be used in the auto-generated changelog.
  • If changes include removal or addition of 3rd party GitHub actions, I updated our internal document. Reach out to the APIx Integration slack channel to get access to the internal document.

Further comments

@lantoli lantoli marked this pull request as ready for review March 12, 2024 14:52
@lantoli lantoli requested a review from a team as a code owner March 12, 2024 14:52
@@ -18,9 +18,13 @@ var (
atlasRegion = "US_EAST_1"
)

func TestMain(m *testing.M) {
Copy link
Member Author

@lantoli lantoli Mar 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Go runs this function before any test in the package

SkipInUnitTest(tb)
require.True(tb, atlasInfo.init, "TestMainExecution must called to be able to use ProjectIDExecution")

atlasInfo.mu.Lock()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we serialize access to avoid problems while creating the project

)

func resourceName() string {
pc, _, _, ok := runtime.Caller(2)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is to get the full function name of the test and extract the resource name

)

// TestMainExecution must be called from TestMain in the test package if ProjectIDExecution is going to be used.
func TestMainExecution(m *testing.M) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggest improving both method names. This one sounds more like an init() and the other one I don't have a suggestion because I am not really sure I understand it. It looks like it's a getProjectID() kind of method

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any suggestion? This method is the implementation of TestMain in the tests, it basically do some setup, execute the tests and teardown.

I called the methods TestMainExecution and ProjectIDExecution with the intention that Execution means during the test execution of the package. So for example ProjectIDExecution provides the id of a project that is created before the tests run and deleted after.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looking at what this file is managing, I think this should be about project resource initialisation.

TestMainExecution --> TestResourceInit
ProjectIDExecution --> GetOrCreateProjectID

I would avoid having this generic TestMain logic when in reality it is solely focusing on project resource re-usage (reminds me of YAGNI).

I still have not digested this file in full (as I mentioned somewhere, it looks too complicated), looping in the rest of the team @maastha @AgustinBettati @EspenAlbert @oarbusi

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TestMainExecution is really for initialising and destroying resources, and calling test runs in the middle.

It's our custom implementation for TestMain adding resource lifecycle management

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

@AgustinBettati AgustinBettati Mar 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do agree that we should focus on more concrete naming so that it is more clear what is being done. I was more on the side of a name such as SetupSharedProject over TestMainExecution.

TestMainExecution is really for initialising and destroying resources

Regarding this point I was slightly confused because this function does not contain all the initialising logic, for example the creation of the project is handled in ProjectIDExecution. Was this for a specific reason?

Copy link
Member Author

@lantoli lantoli Mar 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, because i realized that for instance right now we always run all packages even if one test won't be run. so I lazy initialize the project creation in case test is being executed but doesn't need to get the execution project. in that case we don't need to create the project

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about ProjectIDForExecution or GetProjectIDForCurrentExecution or something?

Copy link
Collaborator

@maastha maastha Mar 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally, I think some methods/files could use more comments as to the "why/how", for example shared_resource.go (or the TestMain() but not sure if we need to do for every resource), as the mechanism could be hard for someone new to understand or use or extend completely or in case we run into any issues later.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about document in our code how Go features like TestMain work, but I'm fine to document more our code. I'm going to merge but please feel free to suggest doc, and i'll think in more docs in the following PRs, thx!

)

// TestMainExecution must be called from TestMain in the test package if ProjectIDExecution is going to be used.
func TestMainExecution(m *testing.M) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't this class also be called by multiple places in parallel?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, Go test calls TestMain and we call this function from there, it's like the main function but when running tests

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok noted. I am curious to know more: TestMain is called once per test package. So if in a subsequent PR you also add func TestMain(m *testing.M) {...} to another package, will this still work?
From how I am getting this (also based on your comment Go runs this function before any test in the package), it looks like there can be multiple TestMain calls from all the different test packages (which I think we have more than one)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, the idea is to add TestMain to all packages.
when you run tests you choose the package, and as TestMain is a public function (uppercase) there can be only one TestMain for that package, and that's the function to be called when running the test for that package.

So it can't happen that two TestMain are run in a test execution, the execution is only for one package, and that can have at most one TestMain


exitCode := m.Run()

if !InUnitTest() && atlasInfo.needsDeletion {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reading this if makes me thinking that we're designing a non-trivial set of rules for which resources should be re-used or deleted. I see a potential risk here that tests could start randomly failing due to concurrency issues and figuring what happened is hard.

Let me start first with:

  1. Why this if? What is the rationale?
  2. Could we make things simpler? e.g. re-use resource for every test file and make it just a serial process?
  3. Is there an alternative way to write this?

Copy link
Member Author

@lantoli lantoli Mar 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

by default a project is created when you call ProjectIDExecution and it's deleted after all tests run. This if deletes the project created for this test execution.

i made a further optimization to allow reuse of a global project for that resource if defined so we avoid creating the project during the test run.

i'm not sure the code can be simplified to fulfil those requirements, but we'll probably learn more as we use this in other resources and might be able to simplify

Copy link
Member Author

@lantoli lantoli Mar 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i've simplified the if condition a little bit, let me know if it helps

1 - the if is to delete the project if it was created before running the tests
2 - we're reusing the resource (project) for all tests in the resource but we allow the tests to run in parallel as now so performance is not affected
3 - surely it can be written in other ways, but I think this approach is ok at the moment, we'll probably learn more soon and will be able to improve it. any suggestions?

atlasInfo.projectName = RandomProjectName()
tb.Logf("Creating execution project: %s, resource: %s, global project (not found): %s\n", atlasInfo.projectName, atlasInfo.resourceName, globalName)
atlasInfo.projectID = createProject(tb, atlasInfo.projectName)
atlasInfo.needsDeletion = true
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this flags that we created a project for the test execution and we'll need to delete it after all tests finish

projectNameMaxLen = 64
)

func resourceName() string {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am probably on the side of making things as simple as possible. Can't we just pass the name of the resource when things are initialised?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this is less error prone than passing the resource name as a string in each call. i think it's better if it can be got automatically.

(something similar happens in the GH Actions where we have some errors from time to time because copying/pasting incorrectly to pass redundant info)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok noted, this method makes me thinking that even if this is code written for testing infra I think it should be unit tested. If we don't test it, I suspect it will become hard to touch for other engineers.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also this method is not really very important, it could fail and return "" and everything would work (except trying to reuse the global project for that resource).

but you have a point, let me see if it can be tested, it's a bit tricky because it depends on the resource test structure.

return matches[1]
}

func createProject(tb testing.TB, name string) string {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] this looks like a more generic util method to be put outside this file.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved

Comment on lines 44 to 56
func ProjectIDGlobal(tb testing.TB) string {
tb.Helper()
return projectID(tb, prefixProjectKeep+"-global")
}

func projectID(tb testing.TB, name string) string {
tb.Helper()
SkipInUnitTest(tb)
resp, _, _ := ConnV2().ProjectsApi.GetProjectByName(context.Background(), name).Execute()
id := resp.GetId()
require.NotEmpty(tb, id, "Project name not found: %s", name)
return id
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[q] Why are we removing the logic of having a Global project? I would imagine we would have a mix of both have a global project, and also projects that are created per resource execution

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because I realized that projects can't have more than 25 clusters so I don't think that's an option any more, at least for generic projects that don't need any special configuration.
for projects than need some setup (like triggers) we'll probably have a global project for triggers.

but in the resource in this PR it doesn't need any special config so i think it's better not to use a global project for it (it can use a global project for that resource, but not shared by all/many resources)

Comment on lines 43 to 46
if atlasInfo.resourceName != "" {
globalName = (prefixProjectKeep + "-" + atlasInfo.resourceName)[:projectNameMaxLen]
globalID = projectID(globalName)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[q] I am struggling to understand in which case resourceName would be empty. If it is empty it is likely an indicator that TestMainExecution was not called, and in this case I would opt for stopping the execution all together.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's just a precaution, imagine a resource doesn't follow the expected structure. but i agree it shouldn't happen but in some edge cases

Comment on lines 54 to 56
atlasInfo.projectName = globalName
tb.Logf("Reusing global project: %s, resource: %s\n", atlasInfo.projectName, atlasInfo.resourceName)
atlasInfo.projectID = globalID
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a possibility of having a separate function for providing the global project? I believe this would help simplify this function and make its usage more clear. In that case I understand that needsDeletion property could be removed as well.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i had in mind that projects just ask for a project and a new one or a resource-global one is given but they don't need to know. and there is a nice fallback mechanism so you can create global-resource clusters for the most demanding resources and maybe for resources that just create a couple of projects you don't want to create a global-resource one as the benefit is not so big and you need that project always created.

do you think it's better to remove this abstraction and projects decide if they want an execution project or a global-project?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in any way there will be a way to ask for specific projects when they need some extra config like triggers, but i don't think it's so useful to allow this for regular projects

@@ -18,9 +18,13 @@ var (
atlasRegion = "US_EAST_1"
)

func TestMain(m *testing.M) {
acc.TestMainExecution(m)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't handle the exist status internally let TestMain handle that which is supposed to do

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's a bit more verbose but I like the idea and it's more explicit, will change to something like:

func TestMain(m *testing.M) {
	acc.SetupSharedResources()
	exitCode := m.Run()
	acc.CleanupSharedResources()
	os.Exit(exitCode)
}

thx!

@lantoli lantoli force-pushed the CLOUDP-237263_project_test_execution branch from b7c7488 to c29b2ce Compare March 13, 2024 14:41
@@ -15,7 +15,7 @@ var (

func TestAccNetworkPrivatelinkEndpointServiceDataFederationOnlineArchiveDS_basic(t *testing.T) {
var (
projectID = acc.ProjectIDGlobal(t)
projectID = acc.ProjectIDExecution(t)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all functions like this will start with ProjectID, we have ProjectIDExecution, ProjectIDGlobal and might have ProjectIDTrigger, etc.

func createProject(tb testing.TB, name string) string {
tb.Helper()
orgID := os.Getenv("MONGODB_ATLAS_ORG_ID")
require.NotNil(tb, "Project creation failed: %s, org not set", name)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
require.NotNil(tb, "Project creation failed: %s, org not set", name)
require.NotNilf(tb, "Project creation failed: %s, org not set", name)

require.NotNil(tb, "Project creation failed: %s, org not set", name)
params := &admin.Group{Name: name, OrgId: orgID}
resp, _, err := ConnV2().ProjectsApi.CreateProject(context.Background(), params).Execute()
require.NoError(tb, err, "Project creation failed: %s, err: %s", name, err)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
require.NoError(tb, err, "Project creation failed: %s, err: %s", name, err)
require.NoErrorf(tb, err, "Project creation failed: %s, err: %s", name, err)

resp, _, err := ConnV2().ProjectsApi.CreateProject(context.Background(), params).Execute()
require.NoError(tb, err, "Project creation failed: %s, err: %s", name, err)
id := resp.GetId()
require.NotEmpty(tb, id, "Project creation failed: %s", name)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
require.NotEmpty(tb, id, "Project creation failed: %s", name)
require.NotEmptyf(tb, id, "Project creation failed: %s", name)

SkipInUnitTest(tb)
resp, _, _ := ConnV2().ProjectsApi.GetProjectByName(context.Background(), name).Execute()
id := resp.GetId()
require.NotEmpty(tb, id, "Project name not found: %s", name)
Copy link
Collaborator

@gssbzn gssbzn Mar 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you make sure all assertions where you do string formatting end with f for format, other wise is a concatenation

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think they do the same, e.g.:

a := "hello"
	require.Empty(t, a, "a is not empty: %s, watchout", a)

Error:      	Should be empty, but was hello
        	Test:       	TestAccNetworkPrivatelinkEndpointServiceDataFederationOnlineArchive_basic
        	Messages:   	a is not empty: hello, watchout

the signature is: func Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) {

example of implementation:

//	assert.NotNilf(t, err, "error message %s", "formatted")
func NotNilf(t TestingT, object interface{}, msg string, args ...interface{}) bool {
	if h, ok := t.(tHelper); ok {
		h.Helper()
	}
	return NotNil(t, object, append([]interface{}{msg}, args...)...)
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think it's just to make sure that the first param is a string (with the msg)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I see internally does fmt.Sprintf(msgAndArgs[0].(string), msgAndArgs[1:]...) I assumed this was closer to t.Error vs t.Errorf 🤷

return id
}

func deleteProject(id string) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

out of curiosity why create is a helper but delete is not?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the creation is done lazily inside a test so we have the T, but deletion is done in TestMain when all test have finished and we don't have test context anymore

func deleteProject(id string) {
_, _, err := ConnV2().ProjectsApi.DeleteProject(context.Background(), id).Execute()
if err != nil {
fmt.Printf("Project deletion failed: %s, error: %s", id, err)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

which is delete is a helper you can then do

Suggested change
fmt.Printf("Project deletion failed: %s, error: %s", id, err)
tb.Logf("Project deletion failed: %s, error: %s", id, err)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't be because it's called from TestMain not from a test

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, this may make this approach a bit noisy on the logs since you usually only want logs when running on verbose mode (-v) and right now with printf is harder to control

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we're running with -v in GH Actions, but you have a point, will consider in next PRs if delete it, thanks!

// CleanupSharedResources must be called from TestMain test package in order to use ProjectIDExecution.
func CleanupSharedResources() {
if sharedInfo.projectID != "" {
fmt.Printf("Deleting execution project: %s, id: %s\n", sharedInfo.projectName, sharedInfo.projectID)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how are we running tests? normally you want logs when running in verbose mode -v

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we use verbose flag:

TF_ACC=1 go test $(ACCTEST_PACKAGES) -run '$(ACCTEST_REGEX_RUN)' -skip '$(ACCTEST_REGEX_SKIP)' -v -parallel $(PARALLEL_GO_TEST) $(TESTARGS) -timeout $(ACCTEST_TIMEOUT) -ldflags="$(LINKER_FLAGS)"

SkipInUnitTest(tb)
resp, _, _ := ConnV2().ProjectsApi.GetProjectByName(context.Background(), name).Execute()
id := resp.GetId()
require.NotEmpty(tb, id, "Project name not found: %s", name)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I see internally does fmt.Sprintf(msgAndArgs[0].(string), msgAndArgs[1:]...) I assumed this was closer to t.Error vs t.Errorf 🤷

func deleteProject(id string) {
_, _, err := ConnV2().ProjectsApi.DeleteProject(context.Background(), id).Execute()
if err != nil {
fmt.Printf("Project deletion failed: %s, error: %s", id, err)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, this may make this approach a bit noisy on the logs since you usually only want logs when running on verbose mode (-v) and right now with printf is harder to control

defer sharedInfo.mu.Unlock()

// lazy creation so it's only done if really needed
if sharedInfo.projectID == "" {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe support os.Getenv("MONGODB_ATLAS_PROJECT_ID") similar to acc.GetClusterInfo?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that is a good idea, but i'm going to do in a next PR so this doesn't get more complicated, thx!

Copy link
Member

@AgustinBettati AgustinBettati left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks for followups simplifying this initial approach

@lantoli lantoli merged commit e054e18 into master Mar 13, 2024
45 checks passed
@lantoli lantoli deleted the CLOUDP-237263_project_test_execution branch March 13, 2024 17:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants